﻿#!/usr/bin/env python
# -*- coding: utf-8 -*-

from constraint import *
from datetime import datetime, date, timedelta
from sets import Set
import logging


def overlapping_time(dates_list1,date_list2):
    for date_elem1 in dates_list1:
        for date_elem2 in date_list2:
            if date_elem1[0] == date_elem2[0]:
                try:
                    time_start1 = datetime.strptime(date_elem1[1],'%H:%M').time()
                    time_start2 = datetime.strptime(date_elem2[1],'%H:%M').time()
                    time_end1 = datetime.strptime(date_elem1[2],'%H:%M').time()
                    time_end2 = datetime.strptime(date_elem2[2],'%H:%M').time()
                except ValueError:
                    return False
                    
                if  datetime.combine(date.today(), time_start1) - datetime.combine(date.today(), time_end2) >=  timedelta() or datetime.combine(date.today(), time_start2) - datetime.combine(date.today(), time_end1) >=  timedelta():
                    continue
                else:
                    return True
    return False
                     

def test_overlap (test_list1,test_list2, dist):
    for test1 in test_list1:
        for test2 in test_list2:
            try:
                date1 = datetime.strptime(test1,"%d/%m/%Y").date()
                date2 = datetime.strptime(test2,"%d/%m/%Y").date()
            except ValueError:
                return False   
            if date1-date2 < timedelta(int(dist)) and date1-date2>=timedelta():
                return True
    return False

def windowHours(sol, orCourses):
    sun = []
    mon = []
    tue = []
    wed = []
    thu = []
    fri = []
    week = [sun,mon,tue,wed,thu,fri]
    sum_of_window = timedelta()
    for course in sol:
        for mifgash in orCourses[course['cNum']+"-"+course['gNum']]:
            if mifgash[0] == u'א':
                sun.append(mifgash[1:])
            if mifgash[0] == u'ב':
                mon.append(mifgash[1:])
            if mifgash[0] == u'ג':
                tue.append(mifgash[1:])
            if mifgash[0] == u'ד':
                wed.append(mifgash[1:])
            if mifgash[0] == u'ה':
                thu.append(mifgash[1:])
            if mifgash[0] == u'ו':
                fri.append(mifgash[1:])
    for day in week:
        if len(day) > 0:
            day.sort(key = lambda x: x[0])
            curr_hour = datetime.strptime(day[0][0],'%H:%M')
            for hours in day:
                sum_of_window = sum_of_window + (datetime.strptime(hours[0],'%H:%M') - curr_hour)
                curr_hour = datetime.strptime(hours[1],'%H:%M')
    return (sum_of_window.total_seconds() /60)/60


def solver(old_courses, constrains):
    #===========================================================================
    # g_optimizeData: {"maxDays":6,"sundayOff":false,"mondayOff":false,
    #             "tuesdayOff":false,"wednesdayOff":false,"thursdayOff":false
    #             ,"fridayOff":false,"minHour":"08:00","maxHour":"22:00",
    #             "examsGap":1,"mustHaveCourses":[]}
    #===========================================================================
    
    
    binary = [0,1]
    days = ['w0','w1','w2','w3','w4','w5']
    not_coming_days = []
    cant_take_courses = set([])
    must_take_courses = []
    tests = Set([])
    sun = Set([])
    mon = Set([])
    tue = Set([])
    wed = Set([])
    thu = Set([])
    fri = Set([])
    courses={}
    coursesTests = []
    main_courses = []
    overlapping = Set([])
    havurot =[]
    size = len(old_courses)
    going = True
    daysOffDict = {u"א" : False,u"ב" : False,u"ג" : False,u"ד" : False,u"ה" : False,u"ו" : False, u"---" : True}
    if constrains['sundayOff']:
        daysOffDict[u"א"] = True
        days.remove('w0')
        not_coming_days.append('w0')
    if constrains['mondayOff']:
        daysOffDict[u"ב"] = True
        days.remove('w1')
        not_coming_days.append('w1')
    if constrains['tuesdayOff']:
        daysOffDict[u"ג"] = True
        days.remove('w2')
        not_coming_days.append('w2')
    if constrains['wednesdayOff']:
        daysOffDict[u"ד"] = True
        days.remove('w3')
        not_coming_days.append('w3')
    if constrains['thursdayOff']:
        daysOffDict[u"ה"] = True
        days.remove('w4')
        not_coming_days.append('w4')
    if constrains['fridayOff']:
        daysOffDict[u"ו"] = True
        days.remove('w5')
        not_coming_days.append('w5')
    must_take_dict = {}
    for man_course in constrains['mustHaveCourses']:
        must_take_dict[man_course] = []
    
    num_to_name_course = {}
    for elem in old_courses:
        num_to_name_course[elem['cNum']] = elem['cName'] 
        for group in elem['kvutzaData']:
                    
                mifgashim = []
                if len(group['days']) == 0:
                    cant_take_courses.add(elem['cNum'] + "-" + group['gNum'])
                for i in range(len(group['days'])):
                    try:
                        if datetime.combine(date.today(), datetime.strptime(group['beginHours'][i],
                                        '%H:%M').time()) - datetime.combine(date.today(), datetime.strptime(constrains['minHour'],
                                                  '%H:%M').time()) <  timedelta() or   datetime.combine(date.today(), datetime.strptime(constrains['maxHour'],
                                                            '%H:%M').time()) - datetime.combine(date.today(), datetime.strptime(group['endHours'][i],
                                                                         '%H:%M').time())  <  timedelta():
                        
                            cant_take_courses.add(elem['cNum'] + "-" + group['gNum'])
                    except ValueError:
                        pass
                    if daysOffDict[group['days'][i]]:
                        cant_take_courses.add(elem['cNum'] + "-" + group['gNum'])
                    if not cant_take_courses.__contains__(elem['cNum'] + "-" + group['gNum']):
                            mifgashim.append((group['days'][i],group['beginHours'][i],group['endHours'][i]))    
                    
                courses[elem['cNum'] + '-' +  group['gNum']] = mifgashim
                if group['kind'] == u'ראשית':
                    if elem['cNum'] in constrains['mustHaveCourses']:
                        if cant_take_courses.__contains__(elem['cNum'] + "-" + group['gNum']):
                            
                            
                            
                            return [0,elem['cName']]
                        
                        
                        
                        must_take_courses.append(elem['cNum'] + "-" + group['gNum'])
                        must_take_dict[elem['cNum']].append(elem['cNum'] + "-" + group['gNum'])
                        
                    main_courses.append(elem['cNum'] + '-' +  group['gNum'])
                    for i in range(len(group['moedType'])):
                        if (group['moedType'][i] == u'בחינה סופית' or group['moedType'][i] == u'בחינת בית' or group['moedType'][i] == u'בחינת ביניים') and not group['dates'][i]=="---" :
                            coursesTests.append((elem['cNum'] + '-' +  group['gNum'],group['dates'][i]))
                    curr_havura = group['havura']
                    havura = []
                    havura_main =[]
                    for elem_group in elem['kvutzaData']:
                        if elem_group['havura'] == curr_havura and elem_group['kind'] == u'משנית':
                            havura.append(elem['cNum'] + '-' +  elem_group['gNum'])
                        if elem_group['havura'] == curr_havura and elem_group['kind'] == u'ראשית':
                            havura_main.append(elem['cNum'] + '-' +  elem_group['gNum'])
                    if len(havura)>=1 and not havurot.__contains__((havura,havura_main)):
                        havurot.append((havura,havura_main))
    for k , v in courses.items():
        for time in v:
            if time[0] == u'א':
                sun.add(k)
            if time[0] == u'ב':
                mon.add(k)
            if time[0] == u'ג':
                tue.add(k)
            if time[0] == u'ד':
                wed.add(k)
            if time[0] == u'ה':
                thu.add(k)
            if time[0] == u'ו':
                fri.add(k)
            for lk , lv in courses.items():
                if not k[:-3] == lk[:-3] and overlapping_time(v,lv):  
                    overlapping.add(Set([k,lk]))
    for course_test1 in coursesTests:
        currTest = Set([])
        for course_test2 in coursesTests:
            if not course_test1[0][:-3]==course_test2[0][:-3] and test_overlap(course_test1[1:],course_test2[1:],int(constrains['examsGap'])):
                currTest.add(course_test2[0])
        if not len(currTest) == 0:
            currTest.add(course_test1[0])
            tests.add(currTest)
    all_main_courses = []
    for main_c in main_courses:
        curr_mains = [elem for elem in main_courses if elem[:-3] == main_c[:-3]]
        if not all_main_courses.__contains__(curr_mains):
            all_main_courses.append(curr_mains)
        
    
          
    while(going and size > 0):
        problem = Problem(BacktrackingSolver())
        problem.addVariables(days, binary)
        problem.addVariables(not_coming_days, [0])
        problem.addVariables([elem for elem in courses.keys() if not must_take_courses.__contains__(elem) and not cant_take_courses.__contains__(elem)], binary)
        problem.addVariables([value[0] for value in must_take_dict.values() if len(value) == 1], [1])
        problem.addVariables(cant_take_courses, [0])
        for value in must_take_dict.values():        
            if len(value)>1:
                problem.addVariables(value,binary)        
                problem.addConstraint(lambda *x : sum(x)==1,value)
        for havura in havurot:
            l = len(havura[1])
            if l == 1:
                problem.addConstraint(lambda *x : sum(x[:-1]) == sum(x[-1:]) ,havura[0]+havura[1])
            if l == 2:
                problem.addConstraint(lambda *x : sum(x[:-2]) == sum(x[-2:]) ,havura[0]+havura[1])
            if l == 3:
                problem.addConstraint(lambda *x : sum(x[:-3]) == sum(x[-3:]) ,havura[0]+havura[1])
            if l == 4:
                problem.addConstraint(lambda *x : sum(x[:-4]) == sum(x[-4:]) ,havura[0]+havura[1])
            if l == 5:
                problem.addConstraint(lambda *x : sum(x[:-5]) == sum(x[-5:]) ,havura[0]+havura[1])
            if l == 6:
                problem.addConstraint(lambda *x : sum(x[:-6]) == sum(x[-6:]) ,havura[0]+havura[1])
            if l == 7:
                problem.addConstraint(lambda *x : sum(x[:-7]) == sum(x[-7:]) ,havura[0]+havura[1])
            if l == 8:
                problem.addConstraint(lambda *x : sum(x[:-8]) == sum(x[-8:]) ,havura[0]+havura[1])
            if l == 9:
                problem.addConstraint(lambda *x : sum(x[:-9]) == sum(x[-9:]) ,havura[0]+havura[1])
            if l == 10:
                problem.addConstraint(lambda *x : sum(x[:-10]) == sum(x[-10:]) ,havura[0]+havura[1])
            if l == 11:
                problem.addConstraint(lambda *x : sum(x[:-11]) == sum(x[-11:]) ,havura[0]+havura[1])
            if l == 12:
                problem.addConstraint(lambda *x : sum(x[:-12]) == sum(x[-12:]) ,havura[0]+havura[1])
            if l == 13:
                problem.addConstraint(lambda *x : sum(x[:-13]) == sum(x[-13:]) ,havura[0]+havura[1])
            if l == 14:
                problem.addConstraint(lambda *x : sum(x[:-14]) == sum(x[-14:]) ,havura[0]+havura[1])
            if l == 15:
                problem.addConstraint(lambda *x : sum(x[:-15]) == sum(x[-15:]) ,havura[0]+havura[1])
            if l == 16:
                problem.addConstraint(lambda *x : sum(x[:-16]) == sum(x[-16:]) ,havura[0]+havura[1])
            if l == 17:
                problem.addConstraint(lambda *x : sum(x[:-17]) == sum(x[-17:]) ,havura[0]+havura[1])
            if l == 18:
                problem.addConstraint(lambda *x : sum(x[:-18]) == sum(x[-18:]) ,havura[0]+havura[1])
            if l == 19:
                problem.addConstraint(lambda *x : sum(x[:-19]) == sum(x[-19:]) ,havura[0]+havura[1])
            if l == 20:
                problem.addConstraint(lambda *x : sum(x[:-20]) == sum(x[-20:]) ,havura[0]+havura[1])
            if l == 21:
                problem.addConstraint(lambda *x : sum(x[:-21]) == sum(x[-21:]) ,havura[0]+havura[1])
            if l == 22:
                problem.addConstraint(lambda *x : sum(x[:-22]) == sum(x[-22:]) ,havura[0]+havura[1])
            if l == 23:
                problem.addConstraint(lambda *x : sum(x[:-23]) == sum(x[-23:]) ,havura[0]+havura[1])
            if l == 24:
                problem.addConstraint(lambda *x : sum(x[:-24]) == sum(x[-24:]) ,havura[0]+havura[1])
            if l == 25:
                problem.addConstraint(lambda *x : sum(x[:-25]) == sum(x[-25:]) ,havura[0]+havura[1])
            if l == 26:
                problem.addConstraint(lambda *x : sum(x[:-26]) == sum(x[-26:]) ,havura[0]+havura[1])
            if l == 27:
                problem.addConstraint(lambda *x : sum(x[:-27]) == sum(x[-27:]) ,havura[0]+havura[1])
            if l == 28:
                problem.addConstraint(lambda *x : sum(x[:-28]) == sum(x[-28:]) ,havura[0]+havura[1])
            if l == 29:
                problem.addConstraint(lambda *x : sum(x[:-29]) == sum(x[-29:]) ,havura[0]+havura[1])
            if l == 30:
                problem.addConstraint(lambda *x : sum(x[:-30]) == sum(x[-30:]) ,havura[0]+havura[1])
        for curr_main_courses in all_main_courses:
            if len(curr_main_courses) > 1:
                problem.addConstraint(lambda *x : sum(x) <= 1 , curr_main_courses)
            
        problem.addConstraint(lambda w,*x : sum(x)>=w,['w0']+list(sun))
        problem.addConstraint(lambda w,*x : sum(x)<=len(x)*w,['w0']+list(sun))
        problem.addConstraint(lambda w,*x : sum(x)>=w,['w1']+list(mon))
        problem.addConstraint(lambda w,*x : sum(x)<=len(x)*w,['w1']+list(mon))                
        problem.addConstraint(lambda w,*x : sum(x)>=w,['w2']+list(tue))
        problem.addConstraint(lambda w,*x : sum(x)<=len(x)*w,['w2']+list(tue))
        problem.addConstraint(lambda w,*x : sum(x)>=w,['w3']+list(wed))
        problem.addConstraint(lambda w,*x : sum(x)<=len(x)*w,['w3']+list(wed))
        problem.addConstraint(lambda w,*x : sum(x)>=w,['w4']+list(thu))
        problem.addConstraint(lambda w,*x : sum(x)<=len(x)*w,['w4']+list(thu))
        problem.addConstraint(lambda w,*x : sum(x)>=w,['w5']+list(fri))
        problem.addConstraint(lambda w,*x : sum(x)<=len(x)*w,['w5']+list(fri))      
        problem.addConstraint(lambda *w : sum(w) <= int(constrains['maxDays']),days)
        for elem in tests:
            colliding_must_courses =Set([]) 
            for course_test in elem:
                
                if must_take_courses.__contains__(course_test):
                    colliding_must_courses.add(course_test[:-3])
            if len(colliding_must_courses) > 1:
                
                
                ret = [num_to_name_course.get(x) for x in colliding_must_courses]
                return [1, ret]
            
            
                
            problem.addConstraint(lambda *x : sum(x)<=1,list(elem))
        
        for elem in overlapping:
            colliding_must_courses =[] 
            for course_test in elem:
                if must_take_courses.__contains__(course_test):
                    colliding_must_courses.append(course_test[:-3])
            if len(colliding_must_courses) > 1:
                
                
                
                ret = [num_to_name_course.get(x) for x in colliding_must_courses]
                return [2, ret]
            
            
            problem.addConstraint(lambda *x : sum(x)<=1,list(elem))
            
        problem.addConstraint(MinSumConstraint(size),main_courses)

        sol = problem.getSolutions()
        
        if len(sol) == 0:
            logging.info(size)
            size = size-1
        else:
            going = False
   
    

    count_days = 0
    c = 3
    best_sols = []
    sorted_by_day_sol = [(sol_course , len([key for key,value in sol_course.items() if key in days and value == 1])) for sol_course in sol]
    sorted_by_day_sol.sort(key=lambda x:x[1])
    for sort_sol in sorted_by_day_sol:
        if sort_sol[1] == count_days:
            best_sols.append(sort_sol[0])
            c= c-1
        else:
            if c <= 0:
                break
            else:
                count_days = sort_sol[1]
                best_sols.append(sort_sol[0])
                c= c-1
    ret = [(sol_course,get_num_of_window(sol_course,days,courses)) for sol_course in best_sols]
    ret.sort(key=lambda x:x[1])
    ret = [x[0] for x in ret]
    real_ret = []
    for final_sol in ret[:3]:
        return_list = []
        for key,value in final_sol.items():
            if  value == 1 and key not in days:
                [cNum,gNum] = key.split('-')
                return_list.append({'cNum' : cNum, 'gNum':gNum})
        real_ret.append(return_list)
    if len(real_ret) > 0: 
        return real_ret[0]
    else: return []
    
    #return real_ret
    

def get_num_of_window(sol_course,days,courses):
    return_list = []
    for key,value in sol_course.items():
        if  value == 1 and key not in days:
            [cNum,gNum] = key.split('-')
            return_list.append({'cNum' : cNum, 'gNum':gNum})
    return windowHours(return_list,courses)

if __name__ == '__main__':  
    x = [{u'language': u' ', u'matalot': u' ', u'faculty': u'\u05de\u05d3\u05e2\u05d9 \u05d4\u05d7\u05d9\u05d9\u05dd', u'hoursNum': 1, u'cYear': u'2015', u'cNum': u'04314999', u'semester': u'2', u'kvutzaData': [{u'endHours': [u'16:00', u'---'], u'startMoedHours': [], u'endMoedHours': [], u'amountHours': 1, u'excellence': [u'', u''], u'gNum': u'01', u'place': [u' ', u' '], u'moed': [], u'kind': u'\u05e8\u05d0\u05e9\u05d9\u05ea', u'moedType': [], u'havura': u'A', u'beginHours': [u'15:00', u'---'], u'ofenHoraa': u'\u05e1\u05de\u05d9\u05e0\u05e8', u'days': [u'\u05d3', u'---'], u'lecturer': [u'\u05d3"\u05e8 \u05e4\u05e8\u05d9\u05d3\u05d4 \u05de\u05ea \u05d1\u05df-\u05e2\u05de\u05d9', u'\u05d3"\u05e8 \u05e4\u05e8\u05d9\u05d3\u05d4 \u05de\u05ea \u05d1\u05df-\u05e2\u05de\u05d9'], u'dates': [], u'matconet': u'---'}], u'isYearly': True, u'video': False, u'havurotNum': u'1', u'students_ids': [], u'depart': u'\u05d6\u05d5\u05d0\u05d5\u05dc\u05d5\u05d2\u05d9\u05d4 ', u'cName': u'\u05e1\u05de\u05d9\u05e0\u05e8 \u05de\u05dc"\u05de \u05d1\' \u05d1\u05d6\u05d5\u05d0\u05d5\u05dc\u05d5\u05d2\u05d9\u05d4', u'zchutHours': u'0'}]
    y = {u'sundayOff': False, u'mondayOff': False, u'minHour': u'08:00', u'wednesdayOff': False, u'maxDays': u'6', u'mustHaveCourses': [], u'thursdayOff': False, u'maxHour': u'22:00', u'tuesdayOff': False, u'fridayOff': False, u'examsGap': u'0'}


    solver(x,y)

